package org.ponly.lpf4j.impl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;

/**
 * 插件类加载器
 *
 * @author vacoor
 */
public class PluginClassLoader extends URLClassLoader {
    private static final Logger LOG = LoggerFactory.getLogger(PluginClassLoader.class);
    private final ClassLoader[] parentClassLoaders;
    private final String pluginId;
    private final String pluginVersion;
    private final List<String> libDirectories;

    public PluginClassLoader(List<URL> urls, ClassLoader[] parents, String pluginId, String version, File pluginRoot) {
        this(urls.toArray(new URL[urls.size()]), parents, pluginId, version, pluginRoot);
    }

    public PluginClassLoader(URL[] urls, ClassLoader[] parents, String pluginId, String version, File pluginRoot) {
        super(urls);
        this.parentClassLoaders = parents;
        this.pluginId = pluginId;
        this.pluginVersion = version;
        this.libDirectories = new ArrayList<String>();

        File libDir = new File(pluginRoot, "lib");
        if (libDir.exists()) {
            this.libDirectories.add(libDir.getAbsolutePath());
        }
    }

    @Override
    public void addURL(URL url) {
        super.addURL(url);
    }

    @Override
    public URL[] getURLs() {
        return super.getURLs();
    }

    public void addUrl(URL url) {
        addURL(url);
    }

    public Collection<URL> getUrls() {
        return Arrays.asList(getURLs());
    }

    /*
    public ClassLoader getParent() {
        return null;
    }
    */

    @Override
    public Class loadClass(String name, final boolean resolve) throws ClassNotFoundException {
        Class c = tryLoadingClass(name, resolve);
        if (c == null) {
            throw new ClassNotFoundException(name + " " + this);
        }
        return c;
    }

    // Changed sequence in which classes are searched, this is essential if plugin uses library,
    // a different version of which is used in IDEA.
    private Class tryLoadingClass(String name, final boolean resolve) {
        Class c = loadClassInsideSelf(name);
        if (c == null) {
            c = loadClassFromParents(name);
        }
        if (c != null) {
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
        return null;
    }

    private synchronized Class loadClassInsideSelf(String name) {
        Class c = findLoadedClass(name);
        if (c != null) {
            return c;
        }
        try {
            c = findClass(name);
        } catch (ClassNotFoundException e) {
            return null;
        }
        return c;
    }

    private Class loadClassFromParents(final String name) {
        for (ClassLoader parent : parentClassLoaders) {
            if (parent instanceof PluginClassLoader) {
                Class c = ((PluginClassLoader) parent).tryLoadingClass(name, false);
                if (c != null) {
                    return c;
                }
                continue;
            }

            try {
                return parent.loadClass(name);
            } catch (ClassNotFoundException ignoreAndContinue) {
                // Ignore and continue
            }
        }

        return null;
    }

    public boolean hasLoadedClass(String name) {
        Class<?> aClass = findLoadedClass(name);
        return aClass != null && aClass.getClassLoader() == this;
    }

    @Override
    public URL findResource(final String name) {
        URL resource = super.findResource(name);
        if (resource != null) {
            return resource;
        }

        for (ClassLoader parent : parentClassLoaders) {
            final URL parentResource = fetchResource(parent, name);
            if (parentResource != null) return parentResource;
        }

        return null;
    }

    @Override
    public InputStream getResourceAsStream(final String name) {
        final InputStream stream = super.getResourceAsStream(name);
        if (stream != null) {
            return stream;
        }

        for (ClassLoader parent : parentClassLoaders) {
            final InputStream inputStream = parent.getResourceAsStream(name);
            if (inputStream != null) {
                return inputStream;
            }
        }

        return null;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Enumeration<URL> findResources(final String name) throws IOException {
        Enumeration<URL>[] resources = new Enumeration[parentClassLoaders.length + 1];
        resources[0] = super.findResources(name);
        for (int idx = 0; idx < parentClassLoaders.length; idx++) {
            resources[idx + 1] = fetchResources(parentClassLoaders[idx], name);
        }
        return new DeepEnumeration(resources);
    }

    @SuppressWarnings("UnusedDeclaration")
    public void addLibDirectories(Collection<String> libDirectories) {
        this.libDirectories.addAll(libDirectories);
    }

    @Override
    protected String findLibrary(String libName) {
        if (!libDirectories.isEmpty()) {
            String libFileName = System.mapLibraryName(libName);
            ListIterator<String> i = libDirectories.listIterator(libDirectories.size());
            while (i.hasPrevious()) {
                File libFile = new File(i.previous(), libFileName);
                if (libFile.exists()) {
                    return libFile.getAbsolutePath();
                }
            }
        }

        return null;
    }

    private static URL fetchResource(ClassLoader cl, String resourceName) {
        try {
            Method findResource = getFindResourceMethod(cl.getClass(), "findResource");
            return findResource != null ? (URL) findResource.invoke(cl, resourceName) : null;
        } catch (Exception e) {
            LOG.error("", e);
            return null;
        }
    }

    private static Enumeration<URL> fetchResources(ClassLoader cl, String resourceName) {
        try {
            Method findResources = getFindResourceMethod(cl.getClass(), "findResources");
            @SuppressWarnings("unchecked") Enumeration<URL> e = findResources == null ? null : (Enumeration) findResources.invoke(cl, resourceName);
            return e;
        } catch (Exception e) {
            LOG.error("", e);
            return null;
        }
    }

    private static Method getFindResourceMethod(final Class<?> clClass, final String methodName) {
        try {
            final Method declaredMethod = clClass.getDeclaredMethod(methodName, String.class);
            declaredMethod.setAccessible(true);
            return declaredMethod;
        } catch (NoSuchMethodException e) {
            final Class superclass = clClass.getSuperclass();
            if (superclass == null || superclass.equals(Object.class)) {
                return null;
            }
            return getFindResourceMethod(superclass, methodName);
        }
    }

    public String getPluginId() {
        return pluginId;
    }

    @Override
    public String toString() {
        return "PluginClassLoader[" + pluginId + ", " + pluginVersion + "]";
    }

    private static class DeepEnumeration implements Enumeration<URL> {
        private final Enumeration<URL>[] enumerations;
        private int index = 0;

        public DeepEnumeration(Enumeration<URL>[] enumerations) {
            this.enumerations = enumerations;
        }

        @Override
        public boolean hasMoreElements() {
            while (index < enumerations.length) {
                Enumeration<URL> e = enumerations[index];
                if (e != null && e.hasMoreElements()) return true;
                index++;
            }
            return false;
        }

        @Override
        public URL nextElement() {
            if (!hasMoreElements()) {
                throw new NoSuchElementException();
            }
            return enumerations[index].nextElement();
        }
    }
}